Skip to content

[pull] main from MetaMask:main#632

Merged
pull[bot] merged 17 commits into
Reality2byte:mainfrom
MetaMask:main
Mar 26, 2026
Merged

[pull] main from MetaMask:main#632
pull[bot] merged 17 commits into
Reality2byte:mainfrom
MetaMask:main

Conversation

@pull
Copy link
Copy Markdown

@pull pull Bot commented Mar 26, 2026

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

kirillzyusko and others added 11 commits March 26, 2026 12:48
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

<!--
Write a short description of the changes included in this pull request,
also include relevant motivation and context. Have in mind the following
questions:
1. What is the reason for the change?
2. What is the improvement/solution?
-->

Replace deprecated `Button` usage with DSRN package.

## **Changelog**

<!--
If this PR is not End-User-Facing and should not show up in the
CHANGELOG, you can choose to either:
1. Write `CHANGELOG entry: null`
2. Label with `no-changelog`

If this PR is End-User-Facing, please write a short User-Facing
description in the past tense like:
`CHANGELOG entry: Added a new tab for users to see their NFTs`
`CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker`

(This helps the Release Engineer do their job more quickly and
accurately)
-->

CHANGELOG entry: null

## **Related issues**

Fixes: https://consensyssoftware.atlassian.net/browse/DSYS-445

## **Manual testing steps**

```gherkin
Feature: my feature name

  Scenario: user [verb for user action]
    Given [describe expected initial app state]

    When user [verb for user action]
    Then [describe expected outcome]
```

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**


https://github.com/user-attachments/assets/8cc0ccb2-5b79-4f37-8d8f-ae326849716d

### **After**


https://github.com/user-attachments/assets/e42afbad-ebe8-41a6-b80d-2bcb0964358e

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches critical onboarding/login entry points by swapping button
components and props; risk is mainly UI/interaction regressions
(disabled/loading/accessibility) rather than logic changes.
> 
> **Overview**
> Migrates several onboarding/authentication screens from the deprecated
component-library `Button` to the design-system `Button` (DSRN),
updating props (`variant`, `isFullWidth`, `isDisabled`, `isLoading`) and
switching to children-based labels.
> 
> Link-style buttons that aren’t yet migrated remain on the old
component (`OldButton`). Tests and snapshots are updated accordingly,
including switching assertions to `toBeDisabled()`/`toBeEnabled()` and
reflecting the new button render/accessibility structure.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
40ba4e7. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
## **Description**

PUMP TP/SL price inputs were blocked at 5 decimal places due to two
independent hardcoded limits. PUMP trades at ~$0.00186, so valid trigger
prices require 6 decimal places (e.g. `0.001234`).

**Root cause (two layers):**
1. `PerpsTPSLView` passed `decimals={TP_SL_VIEW_CONFIG.KeypadDecimals}`
(hardcoded `5`) to `<Keypad>` — the keypad rule silently dropped the 6th
digit
2. `usePerpsTPSLForm` called `hasExceededSignificantFigures(sanitized)`
with default `maxSigFigs=5` — `countSignificantFigures("0.001234")`
returns 6 (counts all decimal digits including leading zeros), blocking
the state update

**Fix:** Replace both hardcoded limits with values from
`DECIMAL_PRECISION_CONFIG`:
- `keypadDecimals` is now computed dynamically from `currentPrice`:
`floor(-log10(price)) + MaxSignificantFigures`, clamped to `[2,
MaxPriceDecimals]`
- `hasExceededSignificantFigures` now uses `MaxPriceDecimals` (=6) as
the limit in both TP and SL handlers

## **Changelog**

CHANGELOG entry: Fixed TP/SL trigger price input for low-price assets
(e.g. PUMP) now accepting up to 6 decimal places

## **Related issues**

Fixes:
[TAT-2403](https://consensyssoftware.atlassian.net/browse/TAT-2403)

## **Manual testing steps**

```gherkin
Feature: PUMP TP/SL trigger price decimal precision

  Scenario: User can enter a 6-decimal TP price for PUMP
    Given I have an open PUMP long position
    When I navigate to the TP/SL screen
    And I focus the Take Profit price input
    And I type "0.001234" via the keypad
    Then the Take Profit price input shows "0.001234"
```

## **Screenshots/Recordings**

### **Before**



https://github.com/user-attachments/assets/cd159c4c-f999-47b8-9c37-dc93dcb79ed2


### **After**

<!-- [screenshots/recordings] -->


https://github.com/user-attachments/assets/c401f733-53b9-4c62-ae9e-6523cf7ac43c


## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

## **Validation Recipe**

<details>
<summary>Automated validation recipe (validate-recipe.sh)</summary>

```json
{
  "pr": "27901",
  "title": "PUMP TP/SL trigger price accepts up to 6 decimal places",
  "jira": "TAT-2403",
  "acceptance_criteria": [
    "TP/SL trigger price input for PUMP accepts up to 6 decimal places (e.g. 0.001234)",
    "Decimal precision is dynamically driven by market price — not a hardcoded global constant",
    "Other markets (e.g. BTC) are unaffected — basic TP/SL via presets still works"
  ],
  "validate": {
    "static": ["yarn lint:tsc"],
    "runtime": {
      "pre_conditions": ["wallet.unlocked"],
      "steps": [
        { "id": "open_pump_position", "action": "flow_ref", "ref": "trade-open-market",
          "params": { "symbol": "PUMP", "side": "long", "usdAmount": "11" } },
        { "id": "wait_position_fill", "action": "wait_for",
          "expression": "Engine.context.PerpsController.getPositions().then(function(ps){var p=ps.filter(function(x){return x.symbol==='PUMP'});return JSON.stringify({count:p.length})})",
          "assert": { "operator": "gt", "field": "count", "value": 0 },
          "timeout_ms": 20000, "poll_ms": 1000 },
        { "id": "create_tpsl_preset", "action": "flow_ref", "ref": "tpsl-create",
          "params": { "symbol": "PUMP", "tpPreset": "25", "slPreset": "-10" } },
        { "id": "nav_tpsl_for_6dec_test", "action": "navigate", "target": "PerpsTPSL",
          "params": { "asset": "PUMP", "currentPrice": 0.00185, "direction": "long" } },
        { "id": "wait_tpsl_screen", "action": "wait_for", "route": "PerpsTPSL" },
        { "id": "focus_tp_input", "action": "eval_sync",
          "expression": "(function(){var hook=globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;var found=null;function walk(f){if(!f)return;var props=f.memoizedProps;if(props&&props.testID===\"perps-tpsl-tp-input\"){found=f;return;}walk(f.child);if(!found)walk(f.sibling);}if(hook&&hook.renderers){hook.renderers.forEach(function(v,k){var roots=hook.getFiberRoots?hook.getFiberRoots(k):null;if(roots)roots.forEach(function(r){if(!found)walk(r.current);});})}if(!found)return \"not-found\";var cur=found.child;while(cur){if(cur.tag===5&&cur.stateNode){var pub=cur.stateNode.canonical&&cur.stateNode.canonical.publicInstance;if(pub&&pub.focus){pub.focus();return \"focused\";}return \"no-focus-method\";}cur=cur.child;}return \"no-host\";})()",
          "assert": { "operator": "eq", "value": "focused" } },
        { "id": "clear_tp_keypad", "action": "clear_keypad", "count": 8 },
        { "id": "type_6decimal_tp_price", "action": "type_keypad", "value": "0.001234" },
        { "id": "assert_tp_6decimal_value", "action": "eval_sync",
          "expression": "(function(){var hook=globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;var found=null;function walk(f){if(!f)return;var props=f.memoizedProps;if(props&&props.testID===\"perps-tpsl-tp-input\"){found=f;return;}walk(f.child);if(!found)walk(f.sibling);}if(hook&&hook.renderers){hook.renderers.forEach(function(v,k){var roots=hook.getFiberRoots?hook.getFiberRoots(k):null;if(roots)roots.forEach(function(r){if(!found)walk(r.current);});})}if(!found)return JSON.stringify({v:\"not-found\"});return JSON.stringify({v:found.memoizedProps&&found.memoizedProps.value||\"no-value\"});})()",
          "assert": { "field": "v", "operator": "contains", "value": "001234" } },
        { "id": "check_no_blocking_errors", "action": "log_watch",
          "window_seconds": 3, "must_not_appear": ["TypeError", "RangeError"] },
        { "id": "screenshot_6decimals", "action": "screenshot", "filename": "pump-tpsl-6-decimals.png" },
        { "id": "cleanup_close_pump", "action": "flow_ref", "ref": "trade-close-position",
          "params": { "symbol": "PUMP" } }
      ]
    }
  }
}
```

</details>

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk UI/input-validation change limited to TP/SL price entry and
rounding; main risk is unintended precision/rounding differences for
some markets when entering prices via keypad or percentage conversions.
> 
> **Overview**
> Fixes TP/SL trigger price entry for very low-priced perps assets by
**removing hardcoded 5-decimal limits**.
> 
> `PerpsTPSLView` now computes `keypadDecimals` dynamically from
`currentPrice` (clamped by `DECIMAL_PRECISION_CONFIG`) and only applies
it to *price* inputs (percentage inputs keep existing precision).
`usePerpsTPSLForm` updates price validation and all RoE→price rounding
to use `DECIMAL_PRECISION_CONFIG.MaxPriceDecimals`, and adds tests
ensuring 6-decimal TP/SL prices (e.g. `0.001234`) are accepted.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
620a45c. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
## **Description**

`URNM` (Sprott Uranium Miners ETF) was listed as `'equity'` in
`HIP3_ASSET_MARKET_TYPES` when it should be `'commodity'`, and `USAR`
(US equity fund) was listed as `'commodity'` when it should be
`'equity'`. This caused both instruments to appear under the wrong
filter tab and display the wrong badge. Fixed by correcting the two
values in the config constant — the single source of truth for HIP-3
market classification.

## **Changelog**

CHANGELOG entry: Fixed incorrect market category assignments for URNM
(now Commodity) and USAR (now Stock) in the Perps market list.

## **Related issues**

Fixes:
[TAT-2499](https://consensyssoftware.atlassian.net/browse/TAT-2499)

## **Manual testing steps**

```gherkin
Feature: Market category filter tabs
  Scenario: URNM appears under Commodities
    Given I am on the Perps market list
    When I tap the "Commodities" filter tab
    Then URNM should appear in the list with a COMMODITY badge

  Scenario: USAR appears under Stocks
    Given I am on the Perps market list
    When I tap the "Stocks" filter tab
    Then USAR should appear in the list with a STOCK badge

  Scenario: URNM does not appear under Stocks
    Given I am on the Perps market list
    When I tap the "Stocks" filter tab
    Then URNM should NOT be visible

  Scenario: USAR does not appear under Commodities
    Given I am on the Perps market list
    When I tap the "Commodities" filter tab
    Then USAR should NOT be visible
```

## **Screenshots/Recordings**

### **Before**
See .task/fix/tat-2499-0325-1917/artifacts/before.mp4

### **After**
See .task/fix/tat-2499-0325-1917/artifacts/after.mp4

## **Validation Recipe**

<details>
<summary>Automated validation recipe (validate-recipe.sh)</summary>

```json
{
  "pr": "27910",
  "title": "URNM and USAR market categories corrected",
  "jira": "TAT-2499",
  "acceptance_criteria": [
    "URNM is categorised as commodity (not equity)",
    "USAR is categorised as equity/stock (not commodity)",
    "Fix is at the config layer, not UI layer"
  ],
  "validate": {
    "static": ["yarn lint:tsc"],
    "runtime": {
      "pre_conditions": ["wallet.unlocked", "perps.feature_enabled"],
      "steps": [
        {
          "id": "nav-market-list",
          "description": "Navigate to market trending list",
          "action": "navigate",
          "target": "PerpsTrendingView"
        },
        {
          "id": "wait-market-list",
          "description": "Wait for market list route to be active",
          "action": "wait_for",
          "route": "PerpsTrendingView"
        },
        {
          "id": "assert-urnm-is-commodity",
          "description": "Assert URNM has marketType=commodity in controller state (the fix)",
          "action": "eval_async",
          "expression": "Engine.context.PerpsController.getMarketDataWithPrices().then(function(markets){var urnm=markets.find(function(market){return market.symbol==='xyz:URNM'});return JSON.stringify({found:!!urnm,marketType:urnm?urnm.marketType:null})})",
          "assert": {
            "operator": "eq",
            "field": "marketType",
            "value": "commodity"
          }
        },
        {
          "id": "assert-usar-is-equity",
          "description": "Assert USAR has marketType=equity in controller state (the fix)",
          "action": "eval_async",
          "expression": "Engine.context.PerpsController.getMarketDataWithPrices().then(function(markets){var usar=markets.find(function(market){return market.symbol==='xyz:USAR'});return JSON.stringify({found:!!usar,marketType:usar?usar.marketType:null})})",
          "assert": {
            "operator": "eq",
            "field": "marketType",
            "value": "equity"
          }
        },
        {
          "id": "nav-commodities-filter",
          "description": "Select Commodities filter tab to verify URNM appears there",
          "action": "flow_ref",
          "ref": "market-discovery",
          "params": {
            "symbol": "xyz:URNM",
            "category": "commodities"
          }
        },
        {
          "id": "assert-urnm-badge-commodity",
          "description": "Verify URNM badge shows commodity type in market detail",
          "action": "eval_async",
          "expression": "Engine.context.PerpsController.getMarketDataWithPrices().then(function(markets){var urnm=markets.find(function(market){return market.symbol==='xyz:URNM'});return JSON.stringify({found:!!urnm,marketType:urnm?urnm.marketType:null,isHip3:urnm?urnm.isHip3:null})})",
          "assert": {
            "operator": "eq",
            "field": "marketType",
            "value": "commodity"
          }
        },
        {
          "id": "nav-stocks-filter",
          "description": "Select Stocks filter tab to verify USAR appears there",
          "action": "flow_ref",
          "ref": "market-discovery",
          "params": {
            "symbol": "xyz:USAR",
            "category": "stocks"
          }
        },
        {
          "id": "assert-usar-badge-equity",
          "description": "Verify USAR badge shows equity type in market detail",
          "action": "eval_async",
          "expression": "Engine.context.PerpsController.getMarketDataWithPrices().then(function(markets){var usar=markets.find(function(market){return market.symbol==='xyz:USAR'});return JSON.stringify({found:!!usar,marketType:usar?usar.marketType:null,isHip3:usar?usar.isHip3:null})})",
          "assert": {
            "operator": "eq",
            "field": "marketType",
            "value": "equity"
          }
        },
        {
          "id": "screenshot-final",
          "description": "Capture final state for human review",
          "action": "screenshot",
          "filename": "after-fix-usar-in-stocks"
        }
      ]
    }
  }
}
```
</details>

## **Pre-merge author checklist**
- [x] I've followed MetaMask Contributor Docs and Coding Standards
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [ ] I've documented my code using JSDoc format if applicable
- [x] I've applied the right labels on the PR

## **Pre-merge reviewer checklist**
- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: updates two static `HIP3_ASSET_MARKET_TYPES` entries and
adds a small unit test to prevent regressions; no runtime logic or
security-sensitive code changes.
> 
> **Overview**
> Fixes HIP-3 perps market classification by swapping the
`HIP3_ASSET_MARKET_TYPES` mappings for `xyz:URNM` (now `commodity`) and
`xyz:USAR` (now `equity`), which affects filter tabs and badges.
> 
> Adds `hyperLiquidConfig.test.ts` to assert correct market-type
mappings for these symbols and a few representative
equity/commodity/forex entries.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
066edfa. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Arthur Breton <abreton@siteed.net>
…n + NFT + Hooks + Misc + perps (#27885)

## **Description**

<!--
Write a short description of the changes included in this pull request,
also include relevant motivation and context. Have in mind the following
questions:
1. What is the reason for the change?
2. What is the improvement/solution?
-->

Part of the analytics migration workstream
([#26686](#26686))
that replaces the legacy `useMetrics` hook with the new `useAnalytics`
hook across the codebase.

This PR (C4) migrates ~18 files covering Simulation Details, NFT
display, Wallet Actions, hooks, and misc components, plus one perps
test-only file.

**Changes per file:**
- Replace `useMetrics` import/call with `useAnalytics`
- Update `MetaMetricsEvents` import paths from `hooks/useMetrics` →
`core/Analytics`
- Rename `addTraitsToUser` → `identify` in `ShowDisplayNFTMediaSheet`
(piggyback rename per workstream plan)
- In tests: replace hand-rolled mock objects with
`createMockUseAnalyticsHook`, use `jest.mocked()` instead of `as
jest.Mock` / `as never`
- Update `MetricsEventBuilder` references to `AnalyticsEventBuilder`
where applicable

## **Changelog**

CHANGELOG entry: null

## **Related issues**

Fixes: #26815
Refs: #26686

## **Manual testing steps**

N/A

## **Screenshots/Recordings**

### **Before**

N/A

### **After**

N/A

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- Generated with the help of the pr-description AI skill -->


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Mostly a mechanical analytics refactor, but it touches user flows
(wallet reset, basic functionality, ramps, NFT detection, simulation
confirmation) where incorrect hook behavior could change
tracking/consent flags or event emission.
> 
> **Overview**
> Migrates several components and hooks from legacy `useMetrics` to the
new `useAnalytics` hook, keeping existing event tracking calls but
updating imports/usages (e.g., `MetaMetricsEvents` paths and
`AnalyticsEventBuilder` in tests).
> 
> Updates tests to mock `useAnalytics` via
`createMockUseAnalyticsHook`/`jest.mocked`, and switches the NFT media
consent sheet from `addTraitsToUser` to `identify` when recording user
traits.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
59b8a7a. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…27923)

## **Description**

<!--
Write a short description of the changes included in this pull request,
also include relevant motivation and context. Have in mind the following
questions:
1. What is the reason for the change?
2. What is the improvement/solution?
-->

Part of the analytics C5 migration phase: replaces `useMetrics` with the
new `useAnalytics` hook in `PredictGTMModal`.

The `useMetrics` hook is being deprecated in favour of `useAnalytics`,
which provides the same `trackEvent` / `createEventBuilder` API through
a unified interface. This commit updates the component and its unit test
to use the new hook and the `createMockUseAnalyticsHook` test helper.

## **Changelog**

<!--
If this PR is not End-User-Facing and should not show up in the
CHANGELOG, you can choose to either:
1. Write `CHANGELOG entry: null`
2. Label with `no-changelog`

If this PR is End-User-Facing, please write a short User-Facing
description in the past tense like:
`CHANGELOG entry: Added a new tab for users to see their NFTs`
`CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker`

(This helps the Release Engineer do their job more quickly and
accurately)
-->

CHANGELOG entry: null

## **Related issues**

Fixes: #27883

## **Manual testing steps**

N/A — pure internal refactor with no user-facing behaviour change.
Existing unit tests cover the analytics event firing.

## **Screenshots/Recordings**

### **Before**

N/A

### **After**

N/A

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- Generated with the help of the pr-description AI skill -->

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk internal refactor that swaps the analytics hook used by
`PredictGTMModal`; behavior should be unchanged aside from potential
wiring/mocking issues in event tracking.
> 
> **Overview**
> Updates `PredictGTMModal` to use the new `useAnalytics` hook instead
of `useMetrics` while keeping the same `trackEvent`/`createEventBuilder`
flow for the modal’s analytics events.
> 
> Refactors `PredictGTMModal.test.tsx` accordingly by mocking
`useAnalytics` and using the shared `createMockUseAnalyticsHook` helper,
plus minor jest mocking cleanup for `StorageWrapper.getItem`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
55c10d9. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…e after SL execution (#27906)

## **Description**

Multi-fill trades (e.g., stop-loss orders split across multiple price
levels by HyperLiquid) showed incorrect PnL and order size on the Perp
Market screen and Perps Home screen. The deduplication key
`orderId-timestamp` used to merge REST and WebSocket fills collapsed
distinct fills with the same orderId+timestamp into one entry, losing
size/PnL data. Fixed by extending the dedup key to
`orderId-timestamp-size-price`, which preserves all distinct fills while
still deduplicating identical entries from both sources. The Activity
page was already correct (no Map-based dedup).

## **Changelog**

CHANGELOG entry: Fixed incorrect PnL and order size for multi-fill
trades on the Perp Market screen and Home screen recent activity

## **Related issues**

Fixes:
[TAT-2483](https://consensyssoftware.atlassian.net/browse/TAT-2483)

## **Manual testing steps**

```gherkin
Feature: Multi-fill trade aggregation consistency
  Scenario: SL execution with multiple fills shows correct aggregated values
    Given the user has an account with SL trades that executed as multiple fills
    When the user navigates to the ETH market page trades tab
    Then the PnL and order size match the Activity page values
    And the PnL and order size match the HyperLiquid UI aggregated view
```

## **Screenshots/Recordings**

### **Before**

### **After**

## **Validation Recipe**

<details>
<summary>Automated validation recipe (validate-recipe.sh)</summary>

```json
{
  "pr": "27906",
  "title": "Verify aggregated PnL and order size consistency across all trade history surfaces",
  "jira": "TAT-2483",
  "acceptance_criteria": [
    "Perp Market screen displays correct aggregated PnL and order size for multi-fill trades",
    "Perps Home screen recent activity shows identical aggregated values",
    "Activity page shows the same aggregated values as the other two surfaces",
    "Multi-fill trades (same orderId + timestamp) are properly aggregated, not collapsed by dedup"
  ],
  "validate": {
    "static": ["yarn lint:tsc"],
    "runtime": {
      "pre_conditions": ["wallet.unlocked", "perps.feature_enabled"],
      "steps": [
        {
          "id": "check_multi_fills_exist",
          "description": "Verify account has multi-fill trades (prerequisite for the bug)",
          "action": "eval_async",
          "expression": "Engine.context.PerpsController.getActiveProviderOrNull().getOrderFills({aggregateByTime: false}).then(function(fills) { var grouped = {}; fills.forEach(function(f) { var key = f.orderId + '_' + f.timestamp; if (!grouped[key]) grouped[key] = 0; grouped[key] = grouped[key] + 1; }); var multiCount = 0; Object.keys(grouped).forEach(function(k) { if (grouped[k] > 1) multiCount = multiCount + 1; }); return JSON.stringify({totalFills: fills.length, multiFillKeys: multiCount}); })",
          "assert": {
            "operator": "gt",
            "field": "multiFillKeys",
            "value": 0
          }
        },
        {
          "id": "verify_new_dedup_preserves_fills",
          "description": "Assert that the fixed dedup key (orderId+timestamp+size+price) preserves all fills - this fails with old key",
          "action": "eval_async",
          "expression": "Engine.context.PerpsController.getActiveProviderOrNull().getOrderFills({aggregateByTime: false}).then(function(fills) { var oldKeyMap = {}; var newKeyMap = {}; fills.forEach(function(f) { var oldKey = f.orderId + '_' + f.timestamp; var newKey = f.orderId + '_' + f.timestamp + '_' + f.size + '_' + f.price; oldKeyMap[oldKey] = f; newKeyMap[newKey] = f; }); var oldCount = Object.keys(oldKeyMap).length; var newCount = Object.keys(newKeyMap).length; return JSON.stringify({totalFills: fills.length, oldKeyUnique: oldCount, newKeyUnique: newCount, oldKeyLost: fills.length - oldCount, newKeyLost: fills.length - newCount}); })",
          "assert": {
            "operator": "eq",
            "field": "newKeyLost",
            "value": 0
          }
        },
        {
          "id": "nav_market_eth",
          "description": "Navigate to ETH market page (has multi-fill SL trades)",
          "action": "flow_ref",
          "ref": "market-discovery",
          "params": { "symbol": "ETH" }
        },
        {
          "id": "wait_market_render",
          "description": "Wait for trades list to render with REST fills",
          "action": "wait",
          "ms": 5000
        },
        {
          "id": "screenshot_market",
          "description": "Capture market trades list for visual review",
          "action": "screenshot",
          "filename": "market-trades-eth"
        },
        {
          "id": "nav_activity",
          "description": "Navigate to activity page trades tab",
          "action": "flow_ref",
          "ref": "activity-view",
          "params": { "tab": "trades" }
        },
        {
          "id": "wait_activity_render",
          "description": "Wait for activity data to load",
          "action": "wait",
          "ms": 3000
        },
        {
          "id": "screenshot_activity",
          "description": "Capture activity trades for visual comparison",
          "action": "screenshot",
          "filename": "activity-trades"
        }
      ]
    }
  }
}
```

</details>

## **Pre-merge author checklist**

- [x] I've followed MetaMask Contributor Docs and Coding Standards
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [ ] I've documented my code using JSDoc format if applicable
- [x] I've applied the right labels on the PR

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes fill deduplication logic used to compute displayed trade
history, which can affect user-visible PnL/size calculations if the new
composite key has edge cases (e.g., precision/format differences for
`size`/`price`). Scope is limited to Perps Home and Market fills merging
plus added tests.
> 
> **Overview**
> Fixes incorrect PnL and order size for stop-loss (multi-fill)
executions by changing REST+WebSocket fill deduplication to key on
`orderId`+`timestamp`+`size`+`price` (instead of just
`orderId`+`timestamp`), so distinct fills at the same timestamp are no
longer collapsed.
> 
> Adds/updates unit tests for `usePerpsHomeData` and
`usePerpsMarketFills` to cover multi-fill preservation and to assert
WebSocket data still wins for *exact* duplicates across sources.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
68d15e8. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…laced components (#27652)

<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

Upgrades `@metamask/design-system-react-native` from `^0.10.0` to
`^0.11.0` and deprecates local component-library components that now
have design system replacements.

https://github.com/MetaMask/metamask-design-system/releases/tag/v25.0.0

**Why:** The design system monorepo v25.0.0 release
(`@metamask/design-system-react-native@0.11.0`) shipped several new
components that duplicate functionality currently in
`app/component-library/`. Upgrading and marking local equivalents as
`@deprecated` guides developers toward the canonical design system
implementations and prevents further adoption of the local versions.

**What changed:**

1. **Package upgrade** — `@metamask/design-system-react-native` bumped
from `^0.10.0` to `^0.11.0`
2. **Breaking change migration** — `ButtonIcon` `isFloating` boolean
prop replaced by `variant` enum in 0.11.0; migrated `InputStepper` to
use `ButtonIconVariant.Floating`
3. **10 components deprecated** with `@deprecated` JSDoc following the
existing codebase pattern, linking to component READMEs and [migration
docs](https://github.com/MetaMask/metamask-design-system/releases):
   - `MainActionButton` (components-temp)
   - `TabEmptyState` (components-temp)
   - `ButtonFilter` (components-temp)
   - `BannerBase` (foundation)
   - `Banner` (union — use `BannerAlert` from DS directly)
   - `BannerAlert` (variant)
   - `BannerTip` (variant — unused, will be removed)
   - `BottomSheet`
   - `BottomSheetDialog` (foundation)
   - `ListItem`

## **Changelog**

CHANGELOG entry: null

## **Related issues**

Fixes:

## **Manual testing steps**

N/A — deprecation annotations are documentation-only changes. The
`ButtonIcon` prop migration in `InputStepper` can be verified by
navigating to the Bridge input stepper UI.

## **Screenshots/Recordings**

### **Before**

N/A

### **After**

N/A

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Upgrades a shared UI dependency and migrates to a changed `ButtonIcon`
API, which could cause subtle UI/regression issues if other call sites
rely on previous props or styles. Password handling was also adjusted to
be controlled in a couple of sensitive SRP/password-entry screens, so
review for unintended behavior changes.
> 
> **Overview**
> Upgrades `@metamask/design-system-react-native` to `0.11.0` (and
`@metamask/design-system-shared` to `0.4.0` via lockfile), and updates
the Bridge `InputStepper` to the new `ButtonIcon` API by replacing
`isFloating` with `variant={ButtonIconVariant.Floating}`.
> 
> Marks multiple in-repo component-library equivalents as **deprecated**
via JSDoc (e.g. `Banner`, `BannerBase`, `BottomSheet`, `ListItem`, and
several `components-temp` components), pointing developers to the
design-system replacements and noting `BannerVariant.Tip`/`BannerTip` as
unused and slated for removal.
> 
> Tightens password input control by initializing `ManualBackupStep1`
password state to `''` and wiring `RevealPrivateCredential`’s
`PasswordEntry` to a new required `password` prop, with corresponding
snapshot updates.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
5987809. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
## **Description**

Update chart URL

## **Changelog**

<!--
If this PR is not End-User-Facing and should not show up in the
CHANGELOG, you can choose to either:
1. Write `CHANGELOG entry: null`
2. Label with `no-changelog`

If this PR is End-User-Facing, please write a short User-Facing
description in the past tense like:
`CHANGELOG entry: Added a new tab for users to see their NFTs`
`CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker`

(This helps the Release Engineer do their job more quickly and
accurately)
-->

CHANGELOG entry: null

## **Related issues**

Fixes:

## **Manual testing steps**

```gherkin
Feature: my feature name

  Scenario: user [verb for user action]
    Given [describe expected initial app state]

    When user [verb for user action]
    Then [describe expected outcome]
```

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**

<!-- [screenshots/recordings] -->

## **Pre-merge author checklist**

- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I've included tests if applicable
- [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk config-only change, but it affects where the in-app advanced
charts load their static assets, so a bad URL would break chart
rendering across builds.
> 
> **Overview**
> Updates `MM_CHARTING_LIBRARY_URL` in `builds.yml` to point to the new
hosted TradingView Advanced Charts asset location
(`charting-assets.static.metamask.io/.../v30.1.0/`) instead of the
previous S3 URL.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
31b605a. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**
Added 'View All' button for all sections in the Explore page 

<!--
Write a short description of the changes included in this pull request,
also include relevant motivation and context. Have in mind the following
questions:
1. What is the reason for the change?
2. What is the improvement/solution?
-->

## **Changelog**

<!--
If this PR is not End-User-Facing and should not show up in the
CHANGELOG, you can choose to either:
1. Write `CHANGELOG entry: null`
2. Label with `no-changelog`

If this PR is End-User-Facing, please write a short User-Facing
description in the past tense like:
`CHANGELOG entry: Added a new tab for users to see their NFTs`
`CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker`

(This helps the Release Engineer do their job more quickly and
accurately)
-->

CHANGELOG entry: added 'View All' button for all sections in the Explore
page

## **Related issues**

Fixes: https://consensyssoftware.atlassian.net/browse/ASSETS-2705

## **Manual testing steps**

- Navigate to the explore page
- Search for anything like "e"
- You should see each section showing a "View all" button
- When clicking on them it should navigate to a screen where it filters
by that query

```gherkin
Feature: my feature name

  Scenario: user [verb for user action]
    Given [describe expected initial app state]

    When user [verb for user action]
    Then [describe expected outcome]
```

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before & After**



https://github.com/user-attachments/assets/50fa43a9-7771-4305-9f2f-f4e6399020b8



## **Pre-merge author checklist**

- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I've included tests if applicable
- [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds a new navigation route/screen and changes Explore search result
rendering and analytics instrumentation, which could affect navigation
flows and event reporting. Risk is moderate since it’s UI/metrics
focused but touches shared section config and touch/scroll handling.
> 
> **Overview**
> Adds a **"View all"** affordance to Explore search section headers
(shown when a section has more than 3 results), limits the inline
section preview to 3 items, and navigates to a new
`ExploreSectionResultsFullView` screen that renders the full section
results.
> 
> Introduces shared Explore search analytics utilities (`TapView`,
`TrackedRowItem`, `useScrollTracking`, `trackExploreEvent`) and a new
MetaMetrics event (`EXPLORE_SEARCH_INTERACTED`) to track taps and
first-scroll interactions in both the search results list and the new
full-results view.
> 
> Updates navigation/types to register the new route
(`Routes.EXPLORE_SECTION_RESULTS_FULL_VIEW`, `RootStackParamList`) and
extends `SECTIONS_CONFIG` with a required `getItemIdentifier` for
consistent item-level analytics; adds/updates unit tests and snapshots
accordingly.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
360771b. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
… support (#27895)

## Summary

- Expand `gasless-swap.spec.ts` with three test scenarios:
  - **ETH → MUSD** (gasless of native token, `gasIncluded: true`) 
  - **USDC → MUSD** (gasless with ERC-20 approval, `gasIncluded: true`) 
  - **ETH → MUSD via 7702** (`gasIncluded7702: true`)


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Medium risk because it monkey-patches `mockttp` matching behavior to
suppress `Error('Aborted')` and expands E2E mocking/fixtures; mistakes
could hide real errors or affect unrelated tests.
> 
> **Overview**
> Adds new gasless swap smoke test scenarios for `MUSD`, including a
live (non-skipped) `ETH → MUSD` flow using `gasIncluded7702`, plus two
additional (currently skipped) cases for standard gasless `ETH → MUSD`
and `USDC → MUSD` with approval.
> 
> Extends swap test fixtures/mocks to support `MUSD` (token lists,
tokens API response, spot prices) and introduces dedicated quote
fixtures for gasless and 7702 quotes (including `txFee` fields required
by validators/UI).
> 
> Hardens E2E mocking by patching `mockttp`’s `matchesAll` to treat
`Error('Aborted')` as a non-match, preventing aborted client requests
from surfacing as unhandled rejections in Jest, and simplifies swap
activity checking by removing an explicit post-toast delay.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
a0acb80. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…7971)

This is mostly about implementing the positions section on the ondo
campaign details page, but it also has some changes to the leaderboard
components.

## **Jira**

[RWDS-1102](http://consensyssoftware.atlassian.net/browse/RWDS-1102)

## **Changelog**

CHANGELOG entry: Ondo GM campaign portfolio positions

## **Screenshots/Recordings**

- No leaderboard position and positions (cta takes them to rwa token
page)

<img width="1011" height="1942" alt="Screenshot from 2026-03-26
13-28-38"
src="https://github.com/user-attachments/assets/19f3b1b9-8344-46d5-98a8-aa0bf5c37591"
/>

- Error loading positions section

<img width="1011" height="1942" alt="Screenshot from 2026-03-26
13-20-08"
src="https://github.com/user-attachments/assets/03905a25-8096-4f91-93b4-bff89884a701"
/>

- Positions loaded, tapping them takes a user to the rwa details page
for that token/network & notice last updated at.

<img width="1031" height="1641" alt="Screenshot from 2026-03-26
12-11-03"
src="https://github.com/user-attachments/assets/a03fff0f-ad66-43a4-ada8-c54275539689"
/>

- Leaderboard rank/position component (no more card layout)

<img width="934" height="1557" alt="Screenshot from 2026-03-26 12-11-12"
src="https://github.com/user-attachments/assets/873747e5-9aeb-46f3-bcef-a6bfd7b252cd"
/>

<img width="1031" height="1641" alt="Screenshot from 2026-03-26
12-11-03"
src="https://github.com/user-attachments/assets/3faf3f05-9181-4c56-8392-5d04a6107d20"
/>

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds a new authenticated portfolio endpoint, cache/state plumbing, and
new UI on the Ondo campaign details screen; moderate risk due to new
data flow and cache invalidation paths in `RewardsController` and Redux
state.
> 
> **Overview**
> Adds an **Ondo GM portfolio “Your Positions”** section to the Ondo
campaign details page (opted-in users only), including
loading/error/empty states and navigation to RWA token list or specific
asset details.
> 
> Extends rewards data flow to fetch/cache portfolio positions:
introduces `useGetOndoPortfolioPosition`, Redux state/actions/selectors
for `ondoCampaignPortfolio`, and a new
`RewardsController:getOndoCampaignPortfolioPosition` action backed by a
new authenticated data-service endpoint
`/ondo-gm/:campaignId/portfolio/me`, with cache invalidation on
opt-in/logout/subscription cache invalidation.
> 
> Refactors Ondo campaign/leaderboard payloads and UI: switches multiple
DTO fields from snake_case to camelCase, adds
`isLeaderboardNotYetComputed` (404) handling with an info banner,
updates `CampaignHowItWorks` to use flat `steps` instead of phased data,
and tightens `CampaignTile` participant-status fetching to active Ondo
campaigns only.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
0e4fc3e. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: sophieqgu <sophieqgu@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@pull pull Bot locked and limited conversation to collaborators Mar 26, 2026
@pull pull Bot added the ⤵️ pull label Mar 26, 2026
Brunonascdev and others added 6 commits March 26, 2026 18:08
#27981)

<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

Adds **wallet balance context** to the Card **`CARD_VIEWED`** event when
the user opens the **Spending limit** (or enable-token) flow, and aligns
row affordance icons with a **down** chevron on the settings-style rows.

**Why**: Product and analytics need to understand **Linea mUSD
exposure**, which **card-supported asset** has the highest fiat balance
for the current account, and which asset is **top across the whole
wallet** (EVM networks plus Solana) at screen view time—without
inferring this from other screens.

**What changed**:

- **`useSpendingLimit.ts`**
- Calls **`useTokensWithBalance`** twice: **card chain IDs**
(`CARD_CHAIN_IDS`) for `walletTokens`, and **all configured EVM chain
IDs plus Solana** (via **`selectEvmNetworkConfigurationsByChainId`** and
**`cardNetworkInfos.solana.caipChainId`**) for `allWalletTokens`.
  - On **`MetaMetricsEvents.CARD_VIEWED`**, adds:
- **`musd_linea_balance`**: fiat amount for **mUSD on Linea** on the
selected account, or **0** if absent.
- **`top_card_chain_asset`**: `network:symbol` (e.g. `linea:musd`) for
the **highest fiat** token among **card-supported** balances
(`allTokens` intersection), or **`null`** if none / unsupported-only
(e.g. top balance not in card list).
- **`top_wallet_chain_asset`** / **`top_wallet_asset_balance`**: highest
fiat token across **all wallet tokens** and its fiat value (or
**`null`** / **0** when applicable). Network label uses
**`caipChainIdToNetwork`** where mapped, otherwise the CAIP chain id
string.
- Introduces **`toNetworkAsset`** to normalize **`network:symbol`** for
analytics.
- Analytics effect runs after wallet token hooks; dependency array
intentionally limited (with eslint comment) so the viewed snapshot stays
stable at mount.
- **`SpendingLimit.tsx`**: **Account**, **Token**, and **Spending
limit** row trailing icons **`ArrowRight` → `ArrowDown`** for clearer
expand/dropdown affordance.
- **`useSpendingLimit.test.ts`**: Tests for **`musd_linea_balance`**,
**`top_card_chain_asset`** (including empty list, non-card-supported top
token), and **`top_wallet_chain_asset`** /
**`top_wallet_asset_balance`**; clarifies default selector mock comment.

## **Changelog**

CHANGELOG entry: Card Spending limit screen analytics include Linea mUSD
fiat balance, top card-supported asset by fiat, and top wallet-wide
asset by fiat when the screen is viewed.

## **Related issues**

Fixes:

## **Manual testing steps**

```gherkin
Feature: Spending limit viewed analytics and row icons

  Scenario: CARD_VIEWED includes balance properties
    Given I open Spending limit (or enable flow) with a known Linea mUSD balance and other tokens
    When the screen is tracked as viewed
    Then CARD_VIEWED includes musd_linea_balance, top_card_chain_asset, top_wallet_chain_asset, and top_wallet_asset_balance consistent with the selected account and supported tokens

  Scenario: Row chevrons
    When I view the Spending limit settings rows (Account, Token, Spending limit)
    Then each row shows a down chevron affordance instead of a right arrow
```

## **Screenshots/Recordings**

Optional: capture the three rows showing **down** chevrons. For
analytics, verify in debug/logs or analytics tooling that
**`CARD_VIEWED`** payloads include the new properties.

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds new analytics properties derived from wallet token balances and
network configuration, including multi-chain sorting logic and a
one-time firing guard; mistakes could skew metrics or add minor
performance overhead on screen mount.
> 
> **Overview**
> **Enriches the card Spending Limit/Enable flow `CARD_VIEWED` analytics
event** with wallet balance context: `musd_linea_balance`, the
highest-fiat *card-supported* asset (`top_card_chain_asset`), and the
highest-fiat asset across all configured wallet chains plus Solana
(`top_wallet_chain_asset` + `top_wallet_asset_balance`).
> 
> Updates `useSpendingLimit` to fetch balances via
`useTokensWithBalance` for both card chains and all wallet chains,
normalizes `network:symbol` formatting, and delays/fires the screen-view
event **exactly once** after `allTokens` is non-empty to avoid
unsupported/empty snapshots; adds unit tests covering these new metrics.
> 
> Adjusts the Spending Limit settings rows’ trailing affordance icons
from `ArrowRight` to `ArrowDown` for Account/Token/Spending limit rows.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
f1b5724. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

Enforces the previously defined `LIMIT_SESSIONS` limit for WCv2
connections. When this limit (20 connections) is exceeded, the oldest
connection is dropped.

## **Changelog**

CHANGELOG entry: null

## **Related issues**

Fixes: https://consensyssoftware.atlassian.net/browse/WAPI-1356

## **Manual testing steps**

1. Modify this constant to be 2
2. Use the ios expo build
3. Using native browser or QR code, connect to
https://react-app.walletconnect.com/
4. Using native browser or QR code, connect to
https://wagmi-app.vercel.app/
5. In the wallet, go to settings, experimental, wallet connect, and
check that you have these two sessions
6. Using native browser or QR code, connect to https://rainbowkit.com/
(using WC)
7. In the wallet, go to settings, experimental, wallet connect, and
check that you only have 2 sessions with the first one you connected no
longer in the list of active sessions

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the
before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**



https://github.com/user-attachments/assets/703ab57f-5dee-4889-957e-9b7e98b02d80



## **Pre-merge author checklist**

- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I've included tests if applicable
- [ ] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [ ] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds automatic disconnection of an existing WalletConnect v2 session
when new approvals push the active session count over a configured
limit, which could unexpectedly drop a user’s oldest connection. Logic
is small and covered by unit tests, but it affects live connection
management.
> 
> **Overview**
> **Enforces a WalletConnect v2 session cap.** After approving a new
session, `WC2Manager` now calls `enforceSessionLimit()` to ensure active
sessions stay under `AppConstants.WALLET_CONNECT.LIMIT_SESSIONS` by
disconnecting the *oldest* session (based on smallest `expiry`).
> 
> **Adds test coverage** for the new behavior, including cases where the
limit is exceeded (oldest session is disconnected) and where the session
count is at/under the limit (no disconnections).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
bf6c451. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been
completely filled out, and PR status checks have passed at least once.
-->

## **Description**

This PR downgrades `@tanstack/react-query` to `^4.43.0` to allow us to
bring in shared tooling between extension and mobile for queries. This
is a temporary measure until the extension gets to React 18, which is
the minimum version for `@tanstack/react-query@5`.

Most of the breaking changes that impact our existing code is type
related due to `TError` being `unknown` in v4. Additionally a couple of
properties we use have been renamed between the two versions. The
semantics of `isLoading` has also changed for `enabled: false` queries,
but that can be recovered by using `isFetching && isLoading` This PR
adjusts all of these.

## **Changelog**

<!--
If this PR is not End-User-Facing and should not show up in the
CHANGELOG, you can choose to either:
1. Write `CHANGELOG entry: null`
2. Label with `no-changelog`

If this PR is End-User-Facing, please write a short User-Facing
description in the past tense like:
`CHANGELOG entry: Added a new tab for users to see their NFTs`
`CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker`

(This helps the Release Engineer do their job more quickly and
accurately)
-->

CHANGELOG entry: null

## **Related issues**

https://consensyssoftware.atlassian.net/browse/WPC-445

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Moderate risk because it downgrades a core data-fetching library and
adjusts loading/polling semantics across multiple hooks, which could
change UI states or caching behavior if any edge cases were missed.
> 
> **Overview**
> Downgrades `@tanstack/react-query` from v5 to v4 (including lockfile
updates) and updates the app’s `QueryClient` defaults to use v4’s
`cacheTime` option.
> 
> Aligns hooks and tests with v4 API/behavior changes: replaces `gcTime`
usage, updates `keepPreviousData` configuration, switches some status
checks from `isPending` to `isLoading`, adds/propagates `isFetching`,
and standardizes derived `isLoading` as `isLoading && isFetching` for
disabled/manual queries (Card hooks, Ramp hooks, Predict query options,
and related tests).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
cb3d691. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
… signing (#28001)

## **Description**

~2% of Claim/Withdraw transactions are failing with Safe GS026 or GS013
errors. Root cause: `EthQuery` caches query results for up to 15
minutes, which can serve a stale nonce when signing Safe transactions.
This fix calls `invalidateQueryCache` before `prepareClaim` and
`signWithdraw` so the Safe TX is always signed with a fresh nonce.

## **Changelog**

CHANGELOG entry: null

## **Related issues**

Fixes: https://consensyssoftware.atlassian.net/browse/PRED-769

## **Manual testing steps**

```gherkin
Feature: Predict Claim/Withdraw nonce freshness

  Scenario: User claims positions without GS026/GS013 errors
    Given the user has claimable positions on Predict
    And the EthQuery cache contains a stale nonce

    When the user initiates a Claim transaction
    Then the query cache is invalidated before signing the Safe TX
    And the transaction succeeds without GS026 or GS013 errors

  Scenario: User withdraws funds without GS026/GS013 errors
    Given the user has a balance available for withdrawal on Predict
    And the EthQuery cache contains a stale nonce

    When the user initiates a Withdraw transaction
    Then the query cache is invalidated before signing the Safe TX
    And the transaction succeeds without GS026 or GS013 errors
```

## **Screenshots/Recordings**

<!-- Not applicable — no UI changes -->

### **Before**

<!-- N/A -->

### **After**

<!-- N/A -->

## **Pre-merge author checklist**

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches Predict claim/withdraw signing flow; while the change is
small, it alters pre-sign network behavior and could impact transaction
submission timing or error handling.
> 
> **Overview**
> Prevents stale Safe nonces during Predict **Claim** and **Withdraw**
by invalidating the underlying `EthQuery` cache immediately before
`prepareClaim` and `signWithdraw` run.
> 
> This adds an `invalidateQueryCache` call (via
`blockTracker.checkForLatestBlock()`) to force fresh chain state before
signing, reducing intermittent Safe `GS026/GS013` failures.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
2144104. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…27944)

## Description

`removeSession()` revokes permissions via
`permissionsController.revokeAllPermissions()` after cleanup, but
`removeAll()` did not — leaving stale permission entries in the
`PermissionController` for every removed session.

This adds the missing `revokeAllPermissions(session.pairingTopic)` call
in `removeAll()`.

### Note on permission key inconsistency

This PR uses `session.pairingTopic` as the revocation key because that's
the key permissions are created under (`origin: channelId` where
`channelId = pairingTopic` — see `_handleSessionProposal`).

However, `removeSession()` uses `session.topic` instead (line 377),
which is a different value. That call is likely a no-op (silently caught
by the surrounding try/catch). This is a pre-existing bug in
`removeSession` — it should probably also use `session.pairingTopic`.
Left as-is to keep this PR scoped.

## Related issues

Follow-up to #27932

## Manual testing steps

1. Connect multiple dapps via WalletConnect
2. Go to Settings > Experimental > WalletConnect Sessions
3. Verify sessions are listed
4. Trigger `removeAll()` (e.g., via wallet reset flow)
5. Verify permission entries for those sessions are no longer in
`PermissionController.state.subjects`

## Pre-merge author checklist

- [x] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile
Coding
Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [x] I've documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] I've applied the right labels on the PR (see [labeling
guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)).
Not required for external contributors.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: small, well-scoped change that only adds missing permission
cleanup during `WC2Manager.removeAll()`, with a unit test covering the
new behavior.
> 
> **Overview**
> Fixes `WC2Manager.removeAll()` to also revoke WalletConnect v2
permissions for each active session (via
`PermissionController.revokeAllPermissions(session.pairingTopic)`)
before disconnecting, preventing stale permission entries after bulk
session removal.
> 
> Adds a focused unit test asserting permission revocation is triggered
during `removeAll()`, and logs (without failing) if revocation throws.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
e8b281e. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Jiexi Luan <jiexiluan@gmail.com>
…it price (#27907)

## **Description**

Limit price presets (Mid, Bid, Ask, -1%, -2%) hardcoded
`formatWithSignificantDigits(value, 4)` — 4 significant digits. For
XRP-range prices (~$2.34), this truncated values to 3 decimal places
($2.342) instead of the expected 4 ($2.3418). Fixed by using
`DECIMAL_PRECISION_CONFIG.MaxSignificantFigures` (= 5), matching the
HyperLiquid API limit and the `PRICE_RANGES_UNIVERSAL` display config.
Also added testIDs to all preset buttons for automated testing.

## **Changelog**

CHANGELOG entry: Fixed limit price preset buttons (Mid, Bid, Ask,
percentage) truncating one decimal place for low-price assets like XRP

## **Related issues**

Fixes:
[TAT-2399](https://consensyssoftware.atlassian.net/browse/TAT-2399)

## **Manual testing steps**

```gherkin
Feature: Limit price preset decimal precision
  Scenario: Mid preset shows correct decimals for XRP
    Given I am on the XRP Long Limit order screen
    When I open the limit price bottom sheet
    And I press the Mid preset button
    Then the limit price shows 4 decimal places (e.g., $2.3418)

  Scenario: All presets show correct decimals
    Given I am on the XRP Long Limit order screen
    When I press each preset (Mid, Bid, -1%, -2%)
    Then each preset value has 4 decimal places

  Scenario: Ask preset works for short orders
    Given I am on the XRP Short Limit order screen
    When I press the Ask preset button
    Then the limit price shows 4 decimal places
```

## **Screenshots/Recordings**

### **Before**
Bug confirmed via CDP eval: `formatWithSignificantDigits(2.3418, 4)` →
`2.342` (3 decimals instead of 4)

### **After**



https://github.com/user-attachments/assets/72b3617e-afdf-49c1-bbbb-2b96e176668d



## **Validation Recipe**

<details>
<summary>Automated validation recipe (validate-recipe.sh)</summary>

```json
{
  "pr": "27907",
  "title": "Limit price presets use correct decimal precision (5 sig figs)",
  "jira": "TAT-2399",
  "acceptance_criteria": [
    "Tapping any limit price preset populates the value with market-correct decimal precision",
    "Validated on XRP (reported case) and SOL (different price range)",
    "All five presets covered: Mid, Bid, Ask, -1%, -2%",
    "No regression to manual limit price entry"
  ],
  "validate": {
    "static": ["yarn lint:tsc"],
    "runtime": {
      "pre_conditions": ["wallet.unlocked", "perps.feature_enabled"],
      "steps": [
        {"id": "nav_xrp", "description": "Navigate to XRP market details", "action": "flow_ref", "ref": "market-discovery", "params": {"symbol": "XRP"}},
        {"id": "press_long", "action": "press", "test_id": "perps-market-details-long-button"},
        {"id": "wait_form", "action": "wait_for", "test_id": "perps-order-header-order-type-button"},
        {"id": "press_order_type", "action": "press", "test_id": "perps-order-header-order-type-button"},
        {"id": "wait_type_sheet", "action": "wait_for", "test_id": "perps-order-type-limit"},
        {"id": "press_limit", "action": "press", "test_id": "perps-order-type-limit"},
        {"id": "wait_limit_form", "action": "wait_for", "test_id": "perps-order-view-limit-price-row"},
        {"id": "press_price_row", "action": "press", "test_id": "perps-order-view-limit-price-row"},
        {"id": "wait_price_sheet", "action": "wait_for", "test_id": "keypad-delete-button"},
        {"id": "press_mid_xrp", "action": "press", "test_id": "perps-limit-price-preset-mid"},
        {"id": "wait_mid", "action": "wait", "ms": 500},
        {"id": "check_mid_xrp", "description": "Assert Mid preset >= 4 decimals for XRP", "action": "eval_sync", "expression": "...", "assert": {"operator": "gt", "field": "decimals", "value": 3}},
        {"id": "press_bid_xrp", "action": "press", "test_id": "perps-limit-price-preset-bid"},
        {"id": "check_bid_xrp", "description": "Assert Bid >= 4 decimals", "action": "eval_sync", "assert": {"operator": "gt", "field": "decimals", "value": 3}},
        {"id": "press_pct_minus1_xrp", "action": "press", "test_id": "perps-limit-price-preset--1"},
        {"id": "check_pct_minus1_xrp", "description": "Assert -1% >= 4 decimals", "action": "eval_sync", "assert": {"operator": "gt", "field": "decimals", "value": 3}},
        {"id": "press_pct_minus2_xrp", "action": "press", "test_id": "perps-limit-price-preset--2"},
        {"id": "check_pct_minus2_xrp", "description": "Assert -2% >= 4 decimals", "action": "eval_sync", "assert": {"operator": "gt", "field": "decimals", "value": 3}},
        {"id": "nav_sol", "action": "flow_ref", "ref": "market-discovery", "params": {"symbol": "SOL"}},
        {"id": "check_mid_sol", "description": "SOL no-regression check", "action": "eval_sync", "assert": {"operator": "eq", "field": "isValid", "value": true}},
        {"id": "nav_short_xrp", "action": "flow_ref", "ref": "market-discovery", "params": {"symbol": "XRP"}},
        {"id": "check_ask_xrp", "description": "Assert Ask >= 4 decimals", "action": "eval_sync", "assert": {"operator": "gt", "field": "decimals", "value": 3}}
      ]
    }
  }
}
```

Full recipe: `.task/fix/tat-2399-0325-1840/artifacts/recipe.json`
</details>

## **Pre-merge author checklist**

- [x] I've followed MetaMask Contributor Docs and Coding Standards
- [x] I've completed the PR template to the best of my ability
- [x] I've included tests if applicable
- [ ] I've documented my code using JSDoc format if applicable
- [x] I've applied the right labels on the PR

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described
in the ticket it closes and includes the necessary testing evidence such
as recordings and or screenshots.


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches perps limit order price entry; while the change is small, it
affects how preset prices are computed and could impact order placement
values if incorrect.
> 
> **Overview**
> Fixes limit-price preset buttons (Mid/Bid/Ask and +/- % presets) to
format using `DECIMAL_PRECISION_CONFIG.MaxSignificantFigures` instead of
hardcoded 4 significant digits, preventing truncation for low-priced
assets (e.g., XRP).
> 
> Adds `testID`s for each preset button (including dynamic % presets)
and extends `PerpsLimitPriceBottomSheet` tests to assert the correct
decimal precision for XRP-range prices.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
ecb689b. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
@pull pull Bot merged commit dee9a46 into Reality2byte:main Mar 26, 2026
1 check failed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.